<template>
    <div>
        <h1>大文件上传</h1>
        <div ref="drag" id="drag" style="padding:10px 0">
            <input style="font-size:2em;" type="file" name="file" @change="handleFileChange" />
        </div>
        <div style="margin-bottom:20px;">
            <button class="btn" @click="handleUpload">上 传</button>
            <button class="btn" @click="handleUpload1">慢启动上传</button>
        </div>
        <!-- 方块进度条 -->
        <div class="cube-container" :style="{ width: cubeWidth + 'px' }">
            <div class="cube" v-for="chunk in chunks" :key="chunk.name">
                <div :class="{
                    'uploading': chunk.progress > 0 && chunk.progress < 100,
                    'success': chunk.progress == 100,
                    'error': chunk.progress < 0,
                }" :style="{ height: chunk.progress + '%' }">
                    <i v-if="chunk.progress < 100 && chunk.progress > 1" class="el-icon-loading"
                        style="color:#F56C6C;"></i>
                </div>
            </div>
        </div>
        <div style="font-size:2em;">
            <!-- 上传进度 -->
            <div>分块进度: {{ hashProgress }} %</div>
            <div>上传进度: {{ uploadProgress }} %</div>
        </div>

    </div>
</template>

<script>
import sparkMd5 from 'spark-md5'
import axios from "axios"
const CHUNK_SIZE = 1 * 1024 * 1024 // 1M

axios.defaults.baseURL = "/api"
axios.defaults.timeout = 5000
axios.interceptors.response.use(
    async response => {
        let { data } = response
        return data
    }
)

export default {
    data() {
        return {
            chunks: [],
            file: null,
            hash: null,
            loading: false,
            hashProgress: 0
        };
    },
    computed: {
        cubeWidth() {
            return Math.ceil(Math.sqrt(this.chunks.length)) * 16
        },
        uploadProgress() {
            if (!this.file || !this.chunks.length) return 0;
            const loaded = this.chunks
                .map(item => item.chunk.size * item.progress)
                .reduce((acc, cur) => acc + cur);
            return parseInt((loaded / this.file.size).toFixed(2));
        }
    },
    methods: {
        handleFileChange(e) {
            const [file] = e.target.files;
            if (!file) return;
            this.file = file;
        },
        createFileChunk(file, size = CHUNK_SIZE) {
            // 生成文件块 Blob.slice语法
            const chunks = [];
            let cur = 0;
            while (cur < file.size) {
                chunks.push({ index: cur, file: file.slice(cur, cur + size) });
                cur += size;
            }
            return chunks;
        },
        async calculateHashSample() {
            return new Promise(resolve => {
                const spark = new sparkMd5.ArrayBuffer();
                const reader = new FileReader();
                const file = this.file;
                // 文件大小
                const size = this.file.size;
                let offset = 2 * 1024 * 1024;

                let chunks = [file.slice(0, offset)];

                // 前面100K

                let cur = offset;
                while (cur < size) {
                    // 最后一块全部加进来
                    if (cur + offset >= size) {
                        chunks.push(file.slice(cur, cur + offset));
                    } else {
                        // 中间的 前中后去两个字节
                        const mid = cur + offset / 2;
                        const end = cur + offset;
                        chunks.push(file.slice(cur, cur + 2));
                        chunks.push(file.slice(mid, mid + 2));
                        chunks.push(file.slice(end - 2, end));
                    }
                    // 前取两个子杰
                    cur += offset;
                }
                // 拼接
                reader.readAsArrayBuffer(new Blob(chunks));

                // 最后100K
                reader.onload = e => {
                    spark.append(e.target.result);
                    this.hashProgress = 100
                    resolve(spark.end());
                };
            });
        },
        ext(filename) {
            // 返回文件后缀名
            return filename.split('.').pop()
        },
        sendRequest(chunks, limit = 4) {
            // 应该是并发上传了，每次限制并发次数
            return new Promise((resolve, reject) => {
                const len = chunks.length
                let counter = 0
                // 全局开关
                let isStop = false
                const start = async () => {
                    if (isStop) {
                        // 当其他块有一个传错的，这个开关也将关闭
                        return
                    }
                    // 从头取一个块
                    const task = chunks.shift()
                    if (task) {
                        // 取出需要上传的 form 信息
                        const { form, index } = task
                        try {
                            // 分块上传
                            await axios.post('/upload', form, {
                                onUploadProgress: progress => {
                                    this.chunks[index].progress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
                                }
                            })
                            if (counter == len - 1) {
                                // 最后一个
                                resolve()
                            } else {
                                // 成功上传后 同步执行 counter++，并再次进行start()
                                counter++
                                start()
                            }
                        } catch (e) {
                            // 当前切片报错了
                            // 尝试3次重试机制，重新push到数组中
                            console.log('出错了')
                            // 进度条改成红色
                            this.chunks[index].progress = -1
                            if (task.error < 3) {
                                task.error++
                                // 队首进去 准备重试
                                chunks.unshift(task)
                                start()
                            } else {
                                // 错误3次了 直接结束
                                isStop = true
                                reject()
                            }
                        }

                    }
                }
                while (limit > 0) {
                    // 并发执行4个 start()
                    setTimeout(() => {
                        // 模拟延迟
                        start()
                    }, Math.random() * 2000)

                    limit -= 1
                }
            })
        },
        async mergeRequest() {
            await axios.post("/merge", {
                ext: this.ext(this.file.name),
                size: CHUNK_SIZE,
                hash: this.hash
            });
        },
        async uploadChunks(uploadedList = []) {
            // 用于上传 chunks
            const list = this.chunks
                // 先筛选出 未上传的部分
                .filter(chunk => uploadedList.indexOf(chunk.name) == -1)
                // 再拼装成 {form,index,error} 的对象形式
                .map(({ chunk, name, hash, index }, i) => {
                    const form = new FormData();
                    form.append("chunkname", name)
                    form.append("ext", this.ext(this.file.name))
                    form.append("hash", hash)
                    form.append("file", chunk)

                    return { form, index, error: 0 }
                })
            try {
                await this.sendRequest([...list], 4)
                if (uploadedList.length + list.length === this.chunks.length) {
                    await this.mergeRequest()
                }
            } catch (e) {
                alert('网速不佳，重试试试哈')
            }

        },
        async handleUpload() {
            if (!this.file) {
                alert("请选择文件");
                return;
            }
            let chunks = this.createFileChunk(this.file);

            // 抽样哈希，牺牲一定的准确率 换来效率，hash一样的不一定是同一个文件， 但是不一样的一定不是 
            // 所以可以考虑用来预判
            this.hash = await this.calculateHashSample()

            // 检查文件是否已经上传
            const { uploaded, uploadedList } = await axios.post('/check', {
                ext: this.ext(this.file.name),
                hash: this.hash
            }
            )
            if (uploaded) {
                return alert("秒传:上传成功")
            }
            // 切片

            this.chunks = chunks.map((chunk, index) => {
                // 每一个切片的名字
                const chunkName = this.hash + '-' + index
                return {
                    hash: this.hash,
                    chunk: chunk.file,
                    name: chunkName,
                    index,
                    // 设置进度条
                    progress: uploadedList.indexOf(chunkName) > -1 ? 100 : 0,
                }
            })
            // 传入已经存在的切片清单
            await this.uploadChunks(uploadedList);
        },
        async handleUpload1() {
            // @todo数据缩放的比率 可以更平缓  
            // @todo 并发+慢启动

            // 慢启动上传逻辑 
            const file = this.file
            if (!file) return;
            const fileSize = file.size
            let offset = 0.1 * 1024 * 1024

            let cur = 0
            let count = 0
            this.hash = await this.calculateHashSample();

            while (cur < fileSize) {
                const chunk = file.slice(cur, cur + offset)
                cur += offset
                const chunkName = this.container.hash + "-" + count;
                const form = new FormData();

                form.append("chunkname", chunkName)
                form.append("ext", this.ext(this.file.name))
                form.append("hash", this.hash)
                let start = new Date().getTime()
                await axios.post('/upload', form)
                const now = new Date().getTime()
                const time = ((now - start) / 1000).toFixed(4)
                // 期望10秒一个切片
                let rate = time / 10
                // 速率有最大和最小 可以考虑更平滑的过滤 比如1/tan 
                if (rate < 0.5) rate = 0.5
                if (rate > 2) rate = 2
                // 新的切片大小等比变化
                console.log(`切片${count}大小是${this.format(offset)},耗时${time}秒,是30秒的${rate}倍，修正大小为${this.format(offset / rate)}`)
                offset = parseInt(offset / rate)
                count++
            }
        },
    }
}
</script>

<style scoped lang="scss">
.btn {
    height: 50px;
    font-size: 1.5em;
    box-sizing: border-box;
    border-radius: 5px;
    cursor: pointer;
    margin-right: 20px;
    &:hover{
        background: gray;
    }
}

.cube-container {
    width: 100px;
    overflow: hidden;

    .cube {
        width: 14px;
        height: 14px;
        line-height: 12px;
        border: 1px solid black background #eee;
        float: left;

        >.success {
            background: #67C23A;
        }

        >.uploading {
            background: #409EFF;
        }

        >.error {
            background: #F56C6C;
        }
    }
}

.el-icon-loading {
    &::after {
        border-width: 10px;
        border-radius: 50%;
        content: '';
        display: block;
        width: 50px;
        height: 50px;
        border-color: skyblue;
        border-right-color: #fff;
        animation: rotatex 3s linear 0 infinite;
    }
}

@keyframes rotatex {
    0% {
        transform: rotate(0);
    }

    100% {
        transform: rotate(360deg);
    }
}
</style>